#!/usr/bin/env python
from gevent.pywsgi import WSGIHandler, WSGIServer
from gevent.server import StreamServer
from gevent import socket as gsocket, sleep
import web, os
from cStringIO import StringIO
from config import Config
from glog import Logged
import types
from time import time
from smtplib import CRLF
from types import IntType, StringType

render = web.template.render(os.path.dirname(__file__) + '/templates', cache=True)
config = Config()

class Singleton(object):
	_instance = None
	def __new__(cls, *args, **kwargs):
		if not cls._instance:
			cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
		return cls._instance

class NetUtil(Logged):
	lag = None
	sleep_table = None
	chunk_size = 78

	def recv_http(self, socket = None):
		if socket is None:
			# assume client
			socket = self.socket
		a = self.recv()
		if not a.ok: return a
		x = a.str

		poscl = x.lower().find('\r\ncontent-length: ')
		poseoh = x.find('\r\n\r\n')
		if poscl < poseoh and poscl >= 0 and poseoh >= 0:
			poseocl = x.find('\r\n', poscl + 17)
			cl = int(x[poscl + 17:poseocl])
			realdata = x[poseoh + 4:]
			a = self.recv(cl - len(realdata))
			if not a.ok: return a
			realdata += a.str
		else:
			return GAnswer(False, 'HTTP header not found')

		return GAnswer(str = realdata, head = x[:poseoh])

	def get_chunkinig(self, size, socket = None):
		if socket is None:
			# assume client
			socket = self.socket
		t = 0
		total_data=[]
		csize = 8192
		while t != size:
			if size - t < csize:
				csize = size - t 
			data = socket.recv(csize)

			if not data:
				break

			if t == 0:
				csize = len(data)
				t += csize
			else:
				t += len(data)
			
			total_data.append(data)

		return ''.join(total_data)

	def send_text(self, p, socket = None):
		if socket is None:
			# assume client
			socket = self.socket
		sp = p.split(CRLF)
		chunks = len(sp)
		self.log('send_text: size:%d, lines:%d' % (len(p), chunks))
		lag = None
		if self.lag is not None:
			lag = self.lag/(chunks)
		st = {}
		if self.sleep_table is not None:
			for i in self.sleep_table:
				percent, lag = i
				st[int((percent/100.0) * len(sp))] = lag
		i = 0
		t1 = time()
		for chunk in sp:
			try:
				socket.send(chunk + CRLF)
			except Exception, e:
				return GAnswer(False, e[1], rc = e[0])
			if lag is not None:
				t2 = time()
				sleep(lag - (t2 - t1 - lag * i))
			i += 1
			if self.sleep_table is not None:
				if i in st:
					self.log('sleep %fs at line %d' % (st[i], i))
					sleep(st[i])
		return GAnswer(load = 'sent %i bytes of data in %fs' % (len(p), time() - t1))

	def send_chunking(self, p, socket = None, chunk_size = None, lag = None, sleep_table = None):
		if socket is None:
			# assume client
			socket = self.socket
		if chunk_size is None:
			chunk_size = self.chunk_size
		if lag is None:
			lag = self.lag
		if sleep_table is None:
			sleep_table = self.sleep_table

		chunks = len(p)/chunk_size
		self.log('send_chunking: size:%d, chunk_size:%d, chunks:%d' % (len(p), chunk_size, chunks + 1))
		st = {}
		if sleep_table is not None:
			for i in sleep_table:
				percent, lag = i
				st[int((percent/100.0) * chunks)] = lag

		if lag is not None:
			lag = lag/(len(p)/chunk_size)
		
		t1 = time()
		encoded_fp = StringIO(p)
		chunk = encoded_fp.read(chunk_size)
		i = 0
		while chunk:
			try:
				socket.send(chunk)
			except Exception, e:
				self.log_error(e)
				break
			chunk = encoded_fp.read(chunk_size)
			if lag is not None:
				t2 = time()
				sleep(lag - (t2 - t1 - lag * i))

			i += 1
			if sleep_table is not None:
				if i in st:
					self.log('sleep %fs at %d chunk' % (st[i], i))
					sleep(st[i])
		return GAnswer(load = 'sent %i bytes of data in %fs' % (len(p) ,time() - t1))

class GServerUtil:
	def start(self):
		try:
			StreamServer.start(self)
		except Exception, e:
			self.log_error(e)
			return GAnswer(False, e[1], rc = e[0])
		return GAnswer(True, load = 'start listening at: %s' % repr(self.address))
	
	def stop(self, timeout=None):
		self.close()

class GServer(GServerUtil, StreamServer, NetUtil, Logged):
	def __init__(self, pool = None, port = 2775, addr = '', name = 'server', desc = ''):
		self.name = name
		self.desc = desc
		self.port = port
		self.cxs = []
		self.last_error = ''
		self.rr_index = 0
		self._stopped_event = None
		StreamServer.__init__(self, (addr, port), self.handlerWrap, spawn = pool)

	def status_tup(self):
		return [self.name, self.desc]

	def __repr__(self):
		return ("Server:%s, port:%d" % (self.name, self.port))

	def __del__(self):
		self.stop()

	def status_str(self):
		return ('%s\nList of connections:\n%s' % (self.__repr__(), ''.join('%s\n' % repr(i) for i in self.cxs)))

	def handlerWrap(self, socket, address):
		self.log('New connection from: %s' % repr(address))
		self.cxs.append(socket)
		s = ''
		c = 0
		for i in self.cxs:
			s += '%i: [%s]\n' % (c, i)
			c += 1
		self.log('List of connections:\n%s' % s[:-1])
		self.handler(socket, address)
		self.cxs.remove(socket)
		self.log('Closed connection from: %s' % repr(address))

	def handler(self, socket, address):
		pass

	def send_socket(self, socket, p):
		self.log('sending to: %s' % repr(socket))
		try:
			socket.send(p)
		except Exception, e:
			self.log_error(e)

	def send(self, where, what):
		if len(self.cxs) == 0:
			s = 'No connections'
			self.log_warn(s)
			return GAnswer(False, load = s)

		if where == 'rr':
			self.rr_index = (self.rr_index + 1) % len(self.cxs)
			self.send_socket(self.cxs[self.rr_index], what)
		elif where == 'last':
			self.send_socket(self.cxs[-1], what)
		elif where.isdigit():
			self.send_socket(self.cxs[int(where)], what)
		else:
			s = 'Wrong arg where=%s' % where
			self.log_warn(s)
			return GAnswer(False, load = s)
		return GAnswer()

class WSGIHandlerLog(WSGIHandler, Logged):
	name = 'wsgi'
	def __init__(self, socket, address, server, rfile=None):
		if address == '':
			address = ('',0)
		WSGIHandler.__init__(self, socket, address, server, rfile=None)
		
	def log_request(self):
		self.log(self.format_request())

	def log_error(self, msg, *args):
		try:
			message = msg % args
		except Exception:
			message = '%r %r' % (msg, args)
		try:
			message = '%s: %s' % (self.socket, message)
		except Exception:
			pass
		Logged.log_error(self, message)

	# workaround for issue 116
	# http://code.google.com/p/gevent/issues/detail?id=116
	def get_environ(self):
			env = self.server.get_environ()
			env['REQUEST_METHOD'] = self.command
			env['SCRIPT_NAME'] = ''

			if '?' in self.path:
				path, query = self.path.split('?', 1)
			else:
				path, query = self.path, ''
			from urllib import unquote
			env['PATH_INFO'] = unquote(path)
			env['QUERY_STRING'] = query

			if self.headers.typeheader is not None:
				env['CONTENT_TYPE'] = self.headers.typeheader

			length = self.headers.getheader('content-length')
			if length:
				env['CONTENT_LENGTH'] = length
			env['SERVER_PROTOCOL'] = 'HTTP/1.0'

			env['REMOTE_ADDR'] = self.client_address[0]

			for header in self.headers.headers:
				try:
					key, value = header.split(':', 1)
				except Exception:
					continue
				key = key.replace('-', '_').upper()
				if key not in ('CONTENT_TYPE', 'CONTENT_LENGTH'):
					value = value.strip()
					key = 'HTTP_' + key
					if key in env:
						if 'COOKIE' in key:
							env[key] += '; ' + value
						else:
							env[key] += ',' + value
					else:
						env[key] = value

			if env.get('HTTP_EXPECT') == '100-continue':
				socket = self.socket
			else:
				socket = None
			chunked = env.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked'
			from gevent.pywsgi import Input
			self.wsgi_input = Input(self.rfile, self.content_length, socket=socket, chunked_input=chunked)
			env['wsgi.input'] = self.wsgi_input
			return env

class WSGIServerLog(GServerUtil, WSGIServer, Logged):
	def __init__(self, *args, **kwargs):
		class MyWSGIHandlerLog(WSGIHandlerLog): 
			# new class for each server is needed for proper logging name
			pass
		self.handler_class = MyWSGIHandlerLog
		WSGIServer.__init__(self, *args, **kwargs)

class GSeq():
	def __init__(self, start, end, var, prefix):
		self.start = start
		self.end = end + 1
		self.prefix = str(prefix)
		self.var = '$' + var
		self.format = '%s%.' + str(len(str(end))) + 'd'

	def gen(self):
		for i in xrange(self.start, self.end):
			yield self.format % (self.prefix, i)

	def __repr__(self):
		return '%s ... %s' % (self.format % (self.prefix, self.start), self.format % (self.prefix, self.end))

from gevent import Timeout
class GClient(NetUtil, Logged):
	def __init__(self, pool = None, addr = '127.0.0.1', port = 0, ipv6 = False, name = 'client', desc = ''):
		self.name = name
		self.desc = desc
		self.connected = False
		self.port = port
		self.addr = addr
		self.ipv6 = ipv6
		self.pool = pool
		self.socket = None
		self.start_loop = False
		self.keep_loop = True
		self.loop_greenlet = None
		self.loop_timeout = None
		self.id_cnt = 0
		from gevent.queue import Queue
		self.q = Queue()
		self.rest = ''

	def create_socket(self):
		if self.ipv6:
			import socket
			s = gsocket.socket(socket.AF_INET6)
		else:
			s = gsocket.socket()
		return s

	def ssl(self):
		from gevent.ssl import wrap_socket
		self.socket = wrap_socket(self.socket)
		return GAnswer(load = 'SSL started')

	def connect(self):
		if self.connected == True:
			return GAnswer(True, 'Already connected')
		if self.socket == None:
			self.socket = self.create_socket()
		self.log('connecting to %s:%i' % (self.addr, self.port))
		try:
			self.socket.connect((self.addr, self.port))
		except Exception, e:
			return GAnswer(False, e[1], rc = e[0])
		self.connected = True

		self.keep_loop = True
		self.loop_greenlet = self.pool.spawn(self.loop)
		return GAnswer(True, load = 'connected %s' % (self.socket))

	def loop(self):
		self.loop_timeout = Timeout(0.1)
		while self.keep_loop:
			self.loop_timeout.start()
			try:
				r = self._recv()
			except Timeout:
				if not self.connected:
					break
				self.loop_timeout.cancel()
				continue
			except Exception, e:
				self.log_error(e)
				self.connected = False
				break
			else:
				self.loop_timeout.cancel()
				if r is None or len(r) == 0:
					sleep(0.1)
					continue
				else:
					self.q.put_nowait(r)
		self.loop_timeout.cancel()
		if self.socket is not None:
			self.socket.close()
			self.socket = None
			self.connected = False
		self.log('stop receiving loop')

	def _recv(self):
		return self.socket.recv(65536)

	def recv(self, size = None):
		r = self.rest
		if size is None:
			r1 = self.q.get()
			if type(r1) != StringType: return r1
			return GAnswer(str = r + r1)
		while len(r) < size:
			r1 = self.q.get()
			if type(r1) != StringType: return r1
			r += r1
		self.rest = r[size:]
		return GAnswer(str = r[:size])

	def get_line(self):
		r = self.rest
		nlp = r.find('\n')
		while nlp == -1:
			r1 = self.q.get()
			if type(r1) != StringType: return r1
			r += r1
			nlp = r.find('\n')
		self.rest = r[nlp + 1:]
		return GAnswer(str = r[:nlp + 1])

	def get_msg(self, msg):
		pass

	def send(self, data):
		if self.connected:
			try:
				self.socket.send(data)
			except Exception, e:
				self.log_error(e)
				self.connected = False
				return GAnswer(False, e[1], rc = e[0])
			else:
				return GAnswer()
		return GAnswer(False, load = 'not connected')

	def send_sync(self, p):
		r = self.send(p)
		if r.ok: return self.recv()
		return r

	def wait_for_connection(self):
		while self.connected == False:
			sleep(self.reconnect_time)
	
	def close(self):
		s = ''
		if self.loop_greenlet is not None:
			self.keep_loop = False
			self.loop_timeout.cancel()
			self.loop_greenlet.kill()
		elif self.socket is not None:
			s = str(self.socket) + ' '
			self.socket.close()
			self.socket = None
		self.connected = False
		return GAnswer(load = 'closed %s' % s)

	stop = close


class GAnswer(Logged):
	def __init__(self, ok = True, load = None, rc = 0, str = '', str_reg = '', status = '', other='', timeout = 0, msgs = {}, name = None, op = 0, head = None):
		self.ok = ok
		self.rc = rc
		self.str = str
		self.ret_str_reg = str_reg
		self.status = status
		self.load = load
		self.other = ''
		self.timeout = timeout
		self.struct = ''
		self.msgs = msgs
		self.bold = ''
		self.endc = ''
		self.name = name
		self.op = op
		self.head = head

	def add(self, a, b, addnewline = False):
		if addnewline:
			return '%s%s%s\t: \n%s' % (self.bold, a, self.endc, b)
		return '%s%s%s\t: %s\n' % (self.bold, a, self.endc, b)

	def to_short(self, color = False):
		r = ''
		if self.str is not None and len(self.str) != 0:
			r += repr(self.str) + '\n'
		if self.load is not None and len(self.load) != 0:
			r += self.load + '\n'
		if type(self.rc) == IntType and self.rc != 0:
			if r[-1] == '\n': r = r[:-1]
			r += ' (rc=%d)' % self.rc
		if self.struct != '':
			r += '\n%s\n' % self.struct
		if len(r) > 2 and r[-1] == '\n': r = r[:-1]
		return r

	def to_string(self, color = False):
		if color:
			self.bold = '\033[1m'
			self.endc = '\033[0m'
		else:
			self.bold = ''
			self.endc = ''
		r = ''
		if self.load is not None:
			r += self.add('Load', self.load)
		if self.struct != '':
			r += self.add('Struct', self.struct, True)
		if self.str != '':
			r += self.add('Str', repr(self.str), False)
		if self.rc != '' and self.rc != 0:
			r += self.add('rc', str(self.rc), False)
		if self.ok:
			r += '%sStatus%s\t: OK\n' % (self.bold, self.endc)
		else:
			r += '%sStatus%s\t: FAIL\n' % (self.bold, self.endc)
		return r[:-1]

	def __str__(self):
		return self.to_string()		

def add_method(c, method, name):
	c.__dict__[name] = types.MethodType(method, c)
	
def read_data(file_name):
	if os.path.exists(file_name):
		pass
	elif os.path.exists(config.conf_dir + '/' + file_name):
		file_name = config.conf_dir + '/' + file_name
	else:
		return None

	file_desc = open(file_name, 'r')
	data = file_desc.read()
	file_desc.close()
	return data

def run(action):
	o = config.get_obj(action)
	if o is not None: o.run()

class GStore(dict):
	def __getattr__(self, attr):
		return self[attr]
	def __setattr__(self, name, value):
		self[name] = value

# found at http://susam.in/blog/auth-cram-md5/
# maybe I use it some day
def cram_md5_response(username, password, base64challenge):
	import hmac, hashlib
	return (username + ' ' + hmac.new(password, base64challenge.decode('base64'),
					  hashlib.md5).hexdigest()).encode('base64')

